package org.mashart.pink.m.gps;

import java.util.Vector;

/**
 * Simple GPS info aggregation class. No methods, just public attributes
 */
public class Waypoint {
    /**
     * Radius of the earth
     */
    public static final int r = 6378137;
    /**
     * Maximum of the altitude
     */
    private static final int MAX_ALTITUDE = Integer.MAX_VALUE;
    /**
     * Minimum of the altitude
     */
    private static final int MIN_ALTITUDE = Integer.MIN_VALUE;
    /**
     * Maximum of the speed
     */
    private static final int MAX_SPEED = Integer.MAX_VALUE;
    /**
     * Minimum of the speed
     */
    private static final int MIN_SPEED = 0;
    /**
     * Elapsed milliseconds from UTC 00:00:00.000 of today
     */
    public int timeMS;
    /**
     * some other formats, like GPRMC has date field too
     */
    public String date;
    /**
     * Obiuosly, Latitude
     * TODO do we need double or float would be just fine?
     */
    public double latitude;
    /**
     * Longitude
     * TODO do we need double or float would be just fine?
     */
    public double longitude;
    /**
     * Height
     */
    public int altitude;
    /**
     * Speed, km/h
     */
    public float speed;
    /**
     * Course
     */
    public int course;
    /**
     * True - North, false - South
     */
//    public boolean ns;
    /**
     * True - East, false - West
     */
//    public boolean ew;
    /**
     * false - no fix (prob. not enought satellites, bad coverage); data NOT valid
     * true - data are valid
     */
    public boolean fix;
    /**
     * Number of satellites, usually from 0 to 12
     */
    public short satellites;
    /**
     * Horizontal Accuracy
     */
    public float acch;
    /**
     * Vertical Accuracy
     */
    public float accv;

    /**
     * Some NMEA sentence format have Units
     * TODO do we need this? Reply: this is only used for altitude. Usaually 'M' (meter) is used. Not sure if there is other unit in use.
     */
//    public Character units;
    /**
     * Empty constructor
     */
    public Waypoint() {
        // -1 means timestamp not initilazed yet
        timeMS = -1;
        // "|=" is used to decide if the waypoint is fixed
        fix = false;
        // set possible unavailable fields
        altitude = Integer.MAX_VALUE;
        speed = Float.NaN;
        course = Integer.MAX_VALUE;
    }

    /**
     * Check if the waypoint is valid.
     * "Valid" means: satellites are fiexed, and altitude and speed are available,
     * and the altitude is within the limitation.
     */
    public boolean valid() {
        return fix && 
                !Float.isNaN(speed) &&
                speed >= MIN_SPEED &&
                speed <= MAX_SPEED &&
                course >= 0 &&
                course <= 360 &&
                altitude != Integer.MAX_VALUE &&
                altitude >= MIN_ALTITUDE &&
                altitude <= MAX_ALTITUDE;
    }

    /**
     * Used by Receiver class to display text on device screen
     * about collected GPS info
     */
    public String toString() {
        char ch_ns = latitude > 0 ? 'N' : 'S';
        char ch_ew = longitude > 0 ? 'E' : 'W';
        char ch_fix = fix ? 'Y' : 'N';

        StringBuffer sb = new StringBuffer();
        String s;

        sb.append("UTC: ");
        // if date is unavailable
        if (date != null) {
            String str_date = date.substring(0, 2) + '/' + date.substring(2, 4) + '/' + date.substring(4, 6);
            sb.append(str_date).append(" ");
        }
        sb.append(timeMStoString(timeMS, true, true)).append('\n');

//        sb.append("Lat: ").append(doubleToString(latitude, 7)).append(ch_ns).append('\n');
//        sb.append("Lng: ").append(doubleToString(longitude, 7)).append(ch_ew).append('\n');
//        sb.append("Alt: ").append(altitude).append('\n');

        sb.append("Speed: ");
        if( !Float.isNaN(speed) ){
            sb.append(floatToString(speed, 2));
        }
        sb.append(" km/h").append('\n');

        sb.append("Course: ");
        if(course != Integer.MAX_VALUE){
            sb.append(course);
        }
        sb.append('\n');
        
//        sb.append("Horizontal Accuracy: ").append(acch).append(" m").append('\n');
//        sb.append("Vertical Accuracy: ").append(accv).append(" m").append('\n');
        if (satellites > 0) {
            sb.append("Satellites Number: ").append(satellites).append('\n');
        }
        sb.append("Satellites Fixed: ").append(ch_fix).append('\n');

        s = sb.toString();
        sb = null;
        return s;
    }

    /**
     * Get an array of Strings
     */
    public String [] toStrings() {
        char ch_ns = latitude > 0 ? 'N' : 'S';
        char ch_ew = longitude > 0 ? 'E' : 'W';
        char ch_fix = fix ? 'Y' : 'N';

        StringBuffer sb = new StringBuffer();
        String [] s = new String[4];

        String str_utc;
        String str_spd;
        String str_course;
        String str_satellite;

//        sb.append("UTC: ");
        // if date is unavailable
        if (date != null) {
            String str_date = date.substring(0, 2) + '/' + date.substring(2, 4) + '/' + date.substring(4, 6);
            sb.append(str_date).append(" ");
        }
        sb.append(timeMStoString(timeMS, true, true));
        str_utc = sb.toString();
        sb = null;

//        sb.append("Lat: ").append(doubleToString(latitude, 7)).append(ch_ns).append('\n');
//        sb.append("Lng: ").append(doubleToString(longitude, 7)).append(ch_ew).append('\n');
//        sb.append("Alt: ").append(altitude).append('\n');

        sb = new StringBuffer();
//        sb.append("Speed: ");
        if( !Float.isNaN(speed) ){
            sb.append(floatToString(speed, 2));
        }
//        sb.append(" km/h");
        str_spd = sb.toString();
        sb = null;

        sb = new StringBuffer();
//        sb.append("Course: ");
        if(course != Integer.MAX_VALUE){
            sb.append(course);
        }
        str_course = sb.toString();
        sb = null;

        sb = new StringBuffer();
//        sb.append("Horizontal Accuracy: ").append(acch).append(" m").append('\n');
//        sb.append("Vertical Accuracy: ").append(accv).append(" m").append('\n');
//        sb.append("Satellites: ");
        if (satellites > 0) {
            sb.append(satellites).append('/');
        }
        sb.append(ch_fix);
        str_satellite = sb.toString();
        sb = null;

        s[0] = str_utc;
        s[1] = str_spd;
        s[2] = str_course;
        s[3] = str_satellite;

        return s;
    }


    /**
     * Get the HTTP query.
     * @return query
     */
    public String toHttpQuery() {
        StringBuffer sb = new StringBuffer();
        String s;

        String t = timeMStoString(timeMS, false, false);
        sb.append("t=").append(t);
        sb.append("&lat=").append(doubleToString(latitude, 7));
        sb.append("&lng=").append(doubleToString(longitude, 7));
        sb.append("&alt=").append(altitude);
        sb.append("&v=").append(floatToString(speed, 2));

        s = sb.toString();
        sb = null;
        return s;
    }

//    public static String aggregateHttpQuery( Vector wps, int batchSize ){
//        if ( wps == null || wps.isEmpty() ){
//            return null;
//        }
//
//        String s;
//        StringBuffer sb = new StringBuffer();
//        StringBuffer sb_t = new StringBuffer("t=");
//        StringBuffer sb_lat = new StringBuffer("&lat");
//        StringBuffer sb_lng = new StringBuffer("&lng=");
//        StringBuffer sb_alt = new StringBuffer("&alt=");
//        StringBuffer sb_v = new StringBuffer("&v=");
//
//        Waypoint wp;
//        for( int i = 0 ; i < batchSize && !wps.isEmpty() ; i++ ){
//            if( i > 0 ){
//                sb_t.append(',');
//                sb_lat.append(',');
//                sb_lng.append(',');
//                sb_alt.append(',');
//                sb_v.append(',');
//            }
////            wp = (Waypoint) wps.firstElement();
////            wps.removeElementAt(0);
//            wp = (Waypoint) wps.elementAt(i);
//            sb_t.append(wp.timeMS);
//            sb_lat.append(doubleToString(wp.latitude, 7));
//            sb_lng.append(doubleToString(wp.longitude, 7));
//            sb_alt.append(wp.altitude);
//            sb_v.append(floatToString(wp.speed, 2));
//        }
//
//        sb.append(sb_t).append(sb_lat).append(sb_lng).append(sb_alt).append(sb_v);
//        s = sb.toString();
//        sb = null;
//
//        return s;
//    }

    /**
     * Get the HTTP query of multiple waypoints
     * if the vector is null or empty,
     */
    public static String aggregateHttpQuery(Vector queries) {
        // if there is no element
        if (queries == null || queries.isEmpty()) {
            return null;
        }

        // if there is only one element
        if( queries.size() == 1 ){
            return (String) queries.firstElement();
        }

        // a single query, e.g, "t=123456&lat=56&lng=12&alt=123&v=45"
        String qry;
        // parameters of aggregated query, example of one element: "t=123456,123478"
        StringBuffer[] sb_para_agg = new StringBuffer[5];
        // aggregated query, e.g, "t=123456,123478&lat=56,78&lng=12,34&alt=123,124&v=45,67"
        StringBuffer sb_qry_agg = new StringBuffer();
        // aggregated query
        String qry_agg;
        int indexEqual;
        int indexAnd;

        for (int i = 0; i < sb_para_agg.length; i++) {
            sb_para_agg[i] = new StringBuffer();
        }
        // write header of each parameter
        sb_para_agg[0].append("t=");
        sb_para_agg[1].append("&lat=");
        sb_para_agg[2].append("&lng=");
        sb_para_agg[3].append("&alt=");
        sb_para_agg[4].append("&v=");

        // read the first query
        qry = (String) queries.elementAt(0);

        indexEqual = -1;
        indexAnd = -1;

        // following elements, if any
        int size = queries.size();

        for (int j = 0; j < size; j++) {
            indexEqual = -1;
            indexAnd = -1;
            // read a query
            qry = (String) queries.elementAt(j);
            for (int i = 0; i < sb_para_agg.length; i++) {
                // from the beginning or last occurence of '&', look for the first occurence of '='
                // e.g., for "a=1&b=2&c=3", indexEqual = 1, 5, 9
                indexEqual = qry.indexOf('=', indexAnd + 1);
                // e.g., for "a=1&b=2&c=3", indexAnd = 3, 7, -1 (no more found)
                // from the beginning or last occurence of '=', look for the first occurence of '&'
                indexAnd = qry.indexOf('&', indexEqual + 1);

                // substring between '=' and '&' (or end)
                // e.g., for "a=1&b=2&c=3", paraValue = "1", "2", "3"
                String paraValue;
                if (indexAnd > 0) {
                    paraValue = qry.substring(indexEqual + 1, indexAnd);
                } else {
                    paraValue = qry.substring(indexEqual + 1);
                }

                // seperator of parameters
                if ( j >= 1 ) {
                    sb_para_agg[i].append(",");
                }
                sb_para_agg[i].append(paraValue);
            }
            qry = null;
        }

        // connect the aggregated paramters together
        for (int i = 0; i < sb_para_agg.length; i++) {
            sb_qry_agg.append(sb_para_agg[i].toString());
            sb_para_agg[i] = null;
        }

        qry_agg = sb_qry_agg.toString();
        sb_qry_agg = null;

        return qry_agg;
    }

    /**
     * Get the string representing elapsed milliseconds
     */
    public static String timeMStoString(int ms, boolean withPunctuation, boolean withMillisecond) {
        if (ms < 0) {
            return null;
        }

        // if not withMillisecond, round the millisecond to second
        if (!withMillisecond) {
            ms = round((float) ms / 1000) * 1000;
        }

        // hour
        int hh = ms / 3600000;
        int r2 = ms - 3600000 * hh;

        // minute
        int mm = r2 / 60000;
        int r3 = r2 - 60000 * mm;

        // second
        int ss = r3 / 1000;
        // millisecond
        int mmm = r3 - 1000 * ss;

        StringBuffer sb = new StringBuffer();
        String s;

        if (hh < 10) {
            sb.append(0);
        }
        sb.append(hh);

        if (withPunctuation) {
            sb.append(":");
        }

        if (mm < 10) {
            sb.append(0);
        }
        sb.append(mm);

        if (withPunctuation) {
            sb.append(":");
        }

        if (ss < 10) {
            sb.append("0");
        }
        sb.append(ss);

        if (withMillisecond) {
            if (withPunctuation) {
                sb.append(".");
            }

            if (mmm < 100) {
                sb.append("0");
                if (mmm < 10) {
                    sb.append("0");
                }
            }
            sb.append(mmm);
        }

        s = sb.toString();
        sb = null;
        return s;
    }

    /**
     * Round a float number into integer. If a equals Float.NaN, return Integer.MAX_VALUE
     */
    public static int round(float a) {
        if (Float.isNaN(a)) {
            return Integer.MAX_VALUE;
        }
        return (int) Math.floor(a + 0.5);
    }

    /**
     * Convert a Float value to a String, with the maximum number of fractional digits.
     * @see doubleToString()
     */
    public static String floatToString(float a, int fracDigits) {
        // 1/p is the smallest positive value, which can be represented by fracDigits
        double p = 1;
        for ( int i = 0 ; i < fracDigits ; i++ ) {
            p *= 10;
        }
        if ( Math.abs(a) < 1 / p ) {
            return "0";
        }
        
        // double to string
        String s = String.valueOf(a);
        return fractionTrim(s, fracDigits);
    }

    /**
     * Convert a Double value to a String, with the maximum number of fractional digits.
     * Zeros at the end of fraction are dropped.
     * To keep the code simple, it is assumed that the double value is not in exponential format.
     * Otherwise, return the converted string as it is, the fraction digit limitation and elimination of zeros at end are not done.
     * E.g:</br>
     * <code>doubleToString( 12.123456789, 7 ) = "12.1234567", </code></br>
     * <code>doubleToString( 0.12345, 7 ) = "0.12345", </code></br>
     * <code>doubleToString( 12.123450089, 7 ) = "12.12345", </code></br>
     * <code>doubleToString( 12.000000009, 7 ) = "12", </code></br>
     * <code>doubleToString( 12.0, 7 ) = "12", </code></br>
     * @param a double value
     * @param fracDigits maximum number of fractional digits
     */
    public static String doubleToString(double a, int fracDigits) {
        // 1/p is the smallest positive value, which can be represented by fracDigits
        double p = 1;
        for ( int i = 0 ; i < fracDigits ; i++ ) {
            p *= 10;
        }
        if ( Math.abs(a) < 1 / p ) {
            return "0";
        }
        
        // double to string
        String s = String.valueOf(a);
        return fractionTrim(s, fracDigits);
    }

    /**
     * Trim a string of fraction number, into specified length in fraction.
     * @see doubleToString()
     */
    private static String fractionTrim(String s, int fracDigits) {
        // result
        String re;
        // bytes representing the numeric part of the string, i.e, without chars of and after 'E'/'e'
        byte[] b;

        // if the string is in exponential format, return the converted string as it is
        // this should not happend for a coordinate value
        int indexE = Math.max( s.indexOf('E'), s.indexOf('e') );
        if (indexE != -1) {
            b = s.substring(0, indexE).getBytes();
        } else{
            b = s.getBytes();
        }

        // look for the '.'
        int i = 0;
        while (i < b.length && b[i] != '.') {
            i++;
        }
        // index of '.'
        int indexDot = i;

        // index of the last digit that we want, drop the following digit
        int indexEnd = Math.min(indexDot + fracDigits + 1, b.length);

        // look up the last non-zero digit, drop the zero digits at the end
        int j = indexEnd;
        do {
            j--;
        } while (j > indexDot && b[j] == '0');

        // if now the last digit is '.', drop it
        if (b[j] == '.') {
            j--;
        }

        if ( indexE != -1 ) {
            re = s.substring(0, j + 1) + s.substring(indexE);
        } else {
            re = s.substring(0, j + 1);
        }
        return re;
    }

    /**
     * Approximate atan, positive value only
     * @param t tan value
     * @return angle
     */
    public static int apprAtanPositive(double t) {
        double v [] = {0.2679491924311227, 0.5773502691896257, 1, 1.7320508075688767, 3.7320508075688776};
        int i;
        for ( i = 0 ; i < v.length && v[i] < t ; i++ ) {
        }
        return i * 15;
    }

    /**
     * Approximate atan
     */
    public static int apprAtan(double t){
        if (t > 0) {
            return apprAtanPositive(t);
        } else {
            return -apprAtanPositive(-t);
        }
    }

    /**
     * Calculate the approximate distance between two pairs of coordinates.
     */
    public static void calcDistAndDirection( double lat1, double lon1, double lat2, double lon2, int [] re ) {
        double x1 = Math.toRadians(lat1);
        double x2 = Math.toRadians(lat2);
        double y1 = Math.toRadians(lon1);
        double y2 = Math.toRadians(lon2);
        double s = Math.cos(x1);
        double dx = x2 - x1;
        double dy = y2 - y1;
        re[0] = (int) ( r * Math.sqrt( s * s * dy * dy + dx * dx ) );
        re[1] = apprAtan( dx / (s * dy) );
        if (dy < 0) {
            re[1] += 180;
        }
    }
}